iT邦幫忙

2021 iThome 鐵人賽

DAY 17
0
Modern Web

三十天成為D3.js v7 好手系列 第 17

Day17-D3 的 Scale( ) 比例尺

  • 分享至 

  • xImage
  •  

本篇大綱:Domain & Range 輸入域與輸出域、Interpolate 插補值、continuous & discrete 連續性與離散性、scale 的五種三類、簡單比較

比例尺是 D3 另一個很重要的功能!!因此,今天也會是很長的篇章啦(燦笑摸頭),大家請備好零食飲料,我們準備開始囉!

不知道大家有沒有看過「搞笑的網購商品」這系列呢?就是大家把自己網路上購買的商品 vs 實際收到貨的商品 拍照給大家看,其中有不少的照片長這樣
https://ithelp.ithome.com.tw/upload/images/20210929/20134930aQLe6AUvGH.jpg

是不是超級搞笑,這是給小矮人的椅子嗎?賣家的圖片跟買家實際收到商品有一大段落差,是怎麼回事呢?這是因為賣方並沒有附上比例尺去告知這個椅子應該是多大,買家也就自行腦補正常椅子的比例才導致這樣的結果。我們的生活中充滿各種需要比例尺的時候,像是 google map、商品圖都會使用到比例尺。那這跟D3 圖表到底有什麼關係呢?別急,讓我們繼續看下去

SVG 的 viewport 視窗範圍

之前在svg的篇章中我們有說過,svg 理論上是無限大的,它所設定的 width 跟 height 其實只是 viewport (視窗範圍)。所以,即使我們的數據資料超過 svg 的 viewport,它一樣會存在跟繪製,只是我們看不到而已。就拿以下的範例來說明:

畫面上有個 svg ,它的視窗範圍:高100 X 寬500

  • 當我們的線條長度為300,比 viewport 小:
    https://ithelp.ithome.com.tw/upload/images/20210929/201349302kLQHWtHMK.jpg

  • 當我們的線條長度為600,比 viewport 大:虛線的部分就是仍然存在但我們看不到的地方
    https://ithelp.ithome.com.tw/upload/images/20210929/20134930jZtlj5k3Z6.jpg

由此可知,我們的資料需要在 svg 的 viewport 範圍內,我們才能完整看到所有的資訊,但是我們拿到的資料數據不可能這麼完美的符合svg的可視範圍呀,怎麼辦?這時就要派出 D3 的 scale 上場啦!其實 d3.scale 要做的事情很單純,就是比例換算,但比例換算也牽扯到非常多的細節,我們接著就來仔細的看一下吧!


Scale 在幹嘛?

D3 的 scale 方法是將資料(通常為陣列)轉換成視覺變量(visual variables),例如:位置、長度、顏色等等,這樣一來我們才能使用視覺變量把資料視覺化。舉例而言,scale 可以把資料轉換成以下幾種視覺變量:

  • 轉換成長度 ⇒ 以供長條圖來設定長度
  • 轉換成位置 ⇒ 以供折線圖來設定位置
  • 把百分比資料轉換成連續數值 ⇒ 以供設定顏色的範圍
  • 把時間資料轉換成位置 ⇒ 以供軸線使用

Domain & Range 輸入域與輸出域

要講 scale 之前,我們要先來說說 domain & range (輸入域與輸出域)的概念。剛剛提到 scale 是在進行比例的換算,那既然是「換算」的話,那就要能產生換算前跟換算後的數值吧?這邊的 Domain 跟 Range 就是在處理這個概念

  • Domain 輸入域是在進行比例尺換算前,資料的整個數值範圍
  • Range 輸出域則是進行比例尺換算後,得到換算之後的資料數值範圍

https://ithelp.ithome.com.tw/upload/images/20210929/20134930KnDKWrXp8I.jpg

一般來說,我們會將A範圍換算到B範圍的概念稱為映射。舉例來說,我們設定輸入域是 [0~100] 的範圍,輸出域是 [0~10] 的範圍,設定好這個範圍後當我們輸入數據50時,透過比例尺的換算,最後就會得到5的換算值

// 輸入與輸出比例換算範例
const convert = d3.scaleLinear()
                  .domain([0, 100])
                  .range([0, 10])

console.log(convert(50)); // 5,換算輸出比例完成

由於輸入域跟輸出域是比例尺必備的基本要素,因此每個比例尺的 API 旗下都會有 .domain( ) 跟 .range( )的方法。但看到這邊你有沒有覺得有點疑惑呢?為什麼設定輸入域跟輸出域的範圍後,我們隨便輸入的數值(但要在輸入域內)就能被換算成對應的輸出值呢?這是因為在比例尺的運作中,還使用了另一種重要方法:interpolate 插補值

Interpolate 插補器與插補值

插補是D3的一個蠻重要的應用,但若要詳細解說的話恐怕要另開一個長篇來講,因此我們這邊只會簡單講解它的原理與運作方式,有興趣了解更多的人可以自行上官方文件查看。

插補器說白了就是在兩個值之間平順的插入一些值,這個應用十分廣泛,例如:建立一個平順的動畫效果、設定一個漸層的顏色梯度,這些都是應用了插補的原理。而且 D3 的插補不僅可以應用在數值之間,還可以用在「日期、顏色、字串」等等各種資料型態間,是個非常神奇的功能。

對於插補器我們目前只要了解到這樣就行了,雖然 D3.scale的底層運作使用插補器,但我們並不需要完全了解這個運作也能順利地使用scale,因為困難的地方 D3 都幫我們做掉了XD。大致瞭解插補器在做什麼之後,我們還要知道另一個重要的小知識:連續值與離散值

continuous & discrete 連續性與離散性

連續性跟離散性是我們在使用scale 時也需要瞭解的一個觀念,它直接牽涉到 scale 的分類以及映射方式。先前我們提到 domain 輸入域與 range 輸出域的概念,連續性與離散性指的便是 domain 輸入域與 range 輸出域的映射方式

  • continuous 連續性:指的是資料之間具備關聯的特性,可以用某些運算方式找出彼此的關聯,這類資料通常為數字、日期等等
  • discrete 離散性:指的是則是資料之間並沒有任何關聯,無法用任何運算方式找出彼此的關聯,這類資料通常為字串

將連續性或離散性搭配輸入域與輸出域的概念,就可以得出四種結果

  • continuous input 輸入的資料是連續性資料
  • continuous output 輸出的資料是連續性資料
  • discrete input 輸入的資料是離散性資料
  • discrete output 輸出的資料是離散性資料

上面四種輸入輸出資料的搭配組合,就成了 scale 的主要分類依據,接著來看D3總共提供哪些比例尺的方法吧!


D3 scale 比例尺分類

D3 的官方文件將比例尺分成五種,分別是

  • Continuous Scale 連續性比例尺
  • Sequential Scale 序列比例尺
  • Diverging Scale 發散比例尺
  • Quantize Scale 量化比例尺
  • Ordinal Scale 次序/序位比例尺

但若按照輸入與輸出的資料來分類,這五類比例尺又可以被歸納為三大類:

  • 「連續性資料輸入」與「連續性資料輸出」的比例尺
    包含 Continuous Scale、Sequential Scale、Diverging Scale

  • 「連續性資料輸入」與「離散性資料輸出」的比例尺
    包含Quantize Scale

  • 「離散性資料輸出」與「離散性資料輸出」的比例尺
    包含 Ordinal Scale

以下就讓我們按照這三大類別來看看 D3 scale 的特色吧!


◆ 「連續性資料輸入」與「連續性資料輸出」的比例尺

這一類的比例尺都是將一組連續性的資料,映射到另一個連續性的資料中,並依此對照去進行數值轉換。這一類比例尺又可以被細分成三小類:

Continuous Scale 連續性比例尺

連續性比例尺指的是資料可以被以某種運算方式找到關聯,像是月份的數值遞增、數字與數字間可以透過加減乘除找到規律等等,這些就叫做連續性比例尺;非連續性比例尺則是資料間無法透過運算找出關聯,例如:男女分類、喜歡的寵物(貓、狗、魚、兔)等等。

根據官方文件的敘述,連續性比例尺可以把連續的、定量的 domain (輸入域) 映射到連續的 range (輸出域)。而且如果輸出範圍也是數值,這個映射關係還可以被反轉 (使用 continuous.intert 方法),意思就是我們可以透過反推輸出的值去找到輸入的值。除了反轉之外,連續性比例尺還有這些不同的 API 可以進行相關設定,比較常用的設定我們晚點也會介紹
https://ithelp.ithome.com.tw/upload/images/20210929/20134930Dr3QcptiKR.jpg

不過連續性比例尺只是大分類,不能直接使用,我們要使用它旗下的比例尺方法來進行操作,它旗下的方法包含:

  • scaleLinear 線性比例尺
  • scalePow 冪比例尺
  • scaleLog 對數比例尺
  • scaleIdentity 恆等比例尺
  • scaleRadial 放射比例尺
  • scaleTime 時間比例尺

雖然有這麼多比例尺,但我們比較常用到的只有 scaleLinear 跟 scaleTime 比例尺,我們接下來就仔細的講解一下吧!

★ d3.scaleLinear

線性比例尺是畫圖表時最常用到的比例尺,它最適合將資料轉換成位置或長度,通常會用在繪製折線圖。
https://ithelp.ithome.com.tw/upload/images/20210929/20134930NFZFRJ5vhx.png

線性比例尺的 domain 跟 range 都必須是連續性資料,而且由於是連續性資料,因此可以用陣列帶入最小值與最大值即可,d3.scaleLinear 就會搭配 d3.Interpolate 去自動計算要輸出的數值

let linearScale = d3.scaleLinear()
                  .domain([0, 100])
                  .range([0, 50]);

linearScale(0);   // return 0
linearScale(50);   // returns 25
linearScale(100);  // returns 50

除了轉換長度跟位置之外,線性比例尺也可以用來換算顏色的色度

const colorScale = d3.scaleLinear()
                    .domain([0, 10])
                    .range(['yellow', 'red']);

colorScale(0);   // returns "rgb(255, 255, 0)"
colorScale(5);   // returns "rgb(255, 128, 0)"
colorScale(10);  // returns "rgb(255, 0, 0)"

★ d3.scaleTime

時間比例尺主要是用來換算日期、時間等等資料的方法,它的用法跟線性比例尺很類似,但不同的是時間比例尺的 domain 輸入域必需輸入日期陣列

timeScale = d3.scaleTime()
              .domain([new Date(2021, 0, 1), new Date(2022, 0, 1)])
              .range([0, 700]);

timeScale(new Date(2021, 0, 1));   // returns 0
timeScale(new Date(2021, 6, 1));   // returns 348.00...
timeScale(new Date(2022, 0, 1));   // returns 700

看完連續性比例尺的兩個方法後,接著我們來講講先前提到的細節設定吧

  • continuous.clamp( ) 截斷

    我們瞭解 domain 跟 range的概念,也知道輸入domain範圍內的數字,就能夠被換算成相對應的range數值,但其實如果我們輸入超出domain範圍的數值也一樣能被換算。

    let linearScale = d3.scaleLinear()
      .domain([0, 10])
      .range([0, 100]);
    
    linearScale(20);  // returns 200
    linearScale(-10); // returns -100
    

    如果我們不希望超出domain範圍的數值被換算,就可以使用 continuous.clamp( ) 這個方法,這個方法會將超過的數值直接換成domain 範圍的極端值

    let linearScale = d3.scaleLinear()
      .domain([0, 10])
      .range([0, 100])
      .clamp(true) // 斬斷鎖鏈~~~
    
    linearScale(20);  // returns 100
    linearScale(-10); // returns 0
    
  • continuous.nice( )

    這個 API 是用來延展 domain 的值,讓 domain 的起始值跟終止值變成比較漂亮的數值。有時候我們的domain範圍會直接從後端給的資料抓,但資料不一定是漂亮的數值,這樣反應到軸線上時可能就會讓軸線不那麼漂亮。

    • 要注意的是,這個方法只能用在scale上,而且也只會將數字延展成最接近的完整數值
    let data = [0.243, 0.584, 0.987, 0.153, 0.433];
    let extent = d3.extent(data);
    
    let linearScale = d3.scaleLinear()
      .domain(extent)
      .range([0, 100]);
    

    畫出來的軸線是這樣,由於起始值跟終點值不在X軸線可以設定的範圍內,因此X軸的前後就沒有值 (有關軸線的建立下一篇會講解)

    https://ithelp.ithome.com.tw/upload/images/20210929/20134930lkHkkzVygi.jpg

    這時我們就可以使用 .nice( ) 這個方法讓起始值跟終點值變成漂亮的數值

    let data = [0.243, 0.584, 0.987, 0.153, 0.433];
    let extent = d3.extent(data);
    
    let linearScale = d3.scaleLinear()
      .domain(extent)
      .range([0, 100])
      .nice()
    

    https://ithelp.ithome.com.tw/upload/images/20210929/20134930flcD4P0Zf8.jpg

  • continuous.invert( ) 反推轉換

.invert( ) 這個方法把range的數值換算成domain的數值,通常用在軸線刻度的text顯示,這邊等到軸線的篇章會更仔細講解,目前只要知道它怎麼用就好

let linearScale = d3.scaleLinear()
          .domain([0, 10])
          .range([0, 100]);

linearScale.invert(50);   // returns 5
linearScale.invert(100);  // returns 10

Sequential Scale 序列比例尺

序列比例尺與連續性比例尺和發散比例尺很類似,一樣是將連續數值輸入域映射到連續數值的輸出域。但跟連續性比例尺不同的是: sequential scales 的輸出域是根據指定的內建插補器來進行設定,而且輸出域不可更動、插補方式也不可更動。舉例來說

let sequentialScale = d3.scaleSequential()
  .domain([0, 100])
  .interpolator(d3.interpolateRainbow);

這個例子中,我們設定了domain,但range的部分變成用 d3.interpolator( ) 取代,而且參數帶入d3內建好的 d3.interpolateRainbow 方法用來建立彩虹的色階,我們不可以自己任意改變成 range(...)

let sequentialScale = d3.scaleSequential()
				  .domain([0, 100])
				  .interpolator(d3.interpolateRainbow);

sequentialScale(0);   // returns 'rgb(110, 64, 170)'
sequentialScale(50);  // returns 'rgb(175, 240, 91)'
sequentialScale(100); // returns 'rgb(110, 64, 170)'

D3內建好的顏色插補器除了interpolatorRainbow 之外,還有許多其他不同的方法
https://ithelp.ithome.com.tw/upload/images/20210929/20134930dUl8VaCI5a.jpg

有興趣的可以到 d3-scale-chromatic的官方文件來查看~這邊就不多做說明了

Diverging Scale 發散比例尺

發散比例尺是將一個「連續性、定量的輸入資料」轉換成「連續性、固定的插補器」,不過這個方法我自己到目前為止還沒有使用過~

const spectral = d3.scaleDiverging(d3.interpolateSpectral);

◆ 「連續性資料輸入」與「離散性資料輸出」的比例尺

這一類的比例尺是將一組連續性的資料,映射到另一組離散性的資料中,並依此對照去進行轉換。這一類的比例尺被稱為「量化比例尺」

Quantize Scale 量化比例尺

量化比例尺包含以下幾個比例尺

  • scaleQuantize 量化比例尺
  • scaleQuantile 分位數比例尺
  • scaleThreshold 閾值(臨界值)比例尺

這些API 中比較常用到的是 scaleQuantize 量化比例尺 ,下面我們就來簡單介紹一下

★ d3.scaleQuantize

這個方法使接收一組連續性的數值,並映射到一組離散性的數值中,接著根據離散性數值的數量把連續性數值分成不同區段,再將輸入的數值映射到相對應的區段數值,舉例來說:

let quantizeScale = d3.scaleQuantize()
		  .domain([0, 100])
		  .range(['lightblue', 'orange', 'lightgreen', 'red']);

此處用 scaleQuantize 的方法,會把 [0-100] 的範圍根據range資料切段

— 0-24 ⇒ lightblue
— 25-49 ⇒ orange
— 50-74 ⇒ lightgreen
— 75-100 ⇒ red

我們輸入的數值就會根據這個區段去照到對應的值

quantizeScale(10);  // returns 'lightblue'
quantizeScale(30);  // returns 'orange'
quantizeScale(90);  // returns 'red'

◆ 「離散性資料輸入」與「離散性資料輸出」的比例尺

這類比例尺跟連續性比例尺都很常被使用,也很常拿來互相做比較。這一類的比例尺是將一組離散性的資料,映射到另一組離散性的資料中,並依此對照去進行轉換。由於輸入與輸出的均是離散性資料,而離散性資料之間是沒有相關聯的,因此使用這一類的比例尺時,一定要把要換算的資料一對一搭配好,否則未搭配到的資料就沒有辦法轉換。這一類的比例尺被稱為「次序/序位比例尺」

Ordinal Scale 次序/序位比例尺

次序/序位比例尺又包含以下三種比例尺API:

  • scaleOrdinal 次序比例尺
  • scaleBand 區段比例尺
  • scalePoint 點比例尺

我們來一一說明一下這些API的使用方法

★ d3.scaleOrdinal 次序比例尺

次序比例尺會遍歷輸入的離散性資料 (必須是陣列),並一一映射到輸出的離散性資料(也必須是陣列)。由於數值中沒有關聯性,因此必須將所有要對應的資料都一一列出,如果輸入域的資料比輸出域多的話,輸出域的資料陣列會從頭重複運算

let myData = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']

let ordinalScale = d3.scaleOrdinal()
  .domain(myData)
  .range(['black', 'red', 'green']);

ordinalScale('Jan');  // returns 'black';
ordinalScale('Feb');  // returns 'red';
ordinalScale('Mar');  // returns 'green';
ordinalScale('Apr');  // returns 'black'; range 從頭重複一次

如果輸入的數值不在domain輸入域資料內的話,會自動被加進domain中

ordinalScale('Monday');  // returns 'black';

★ d3.scaleBand 區段比例尺

這個方法最常用來繪製長條圖表,它不僅能用來建立長條狀幾何圖形,也會將圖形間的間距 (padding) 考慮進去。scaleBand 會將輸入域的資料傳換成輸出域的區段
https://ithelp.ithome.com.tw/upload/images/20210929/20134930uAyMHo0BJI.png

domain輸入域的資料必須是陣列,陣列中的每筆資料代表一條長條圖;range 輸出域則定義圖表範圍的最小與最大值 (ex: 整張圖表的寬度)

let bandScale = d3.scaleBand()
  .domain(['狗', '貓', '天竺鼠', '烏龜', '海豚']) // 共有五條長條圖
  .range([0, 200]); // 整張圖表的範圍

scaleBand 這個方法會將 range 根據 domain 的數量去切分區段,然後根據這個區段的資料去計算長條圖的位置與寬度

bandScale('狗'); // returns 0
bandScale('貓'); // returns 40
bandScale('海豚'); // returns 160

scaleBand 也提供了一些細節設定的API,讓我們能依此去設定長條圖的寬度、間距等等
https://ithelp.ithome.com.tw/upload/images/20210929/20134930VayBvLSnzB.jpg

如果要設定長條圖的寬度,我們會使用 .bandwidth( ) 這個方法

bandScale.bandwidth();  // returns 40

要設定長條圖間的間距則有以下兩個 API

  • .paddingInner( ) 每條長條圖之間的距離
  • .paddingOuter( ) 第一條長條圖跟最後一條長條圖的距離

https://ithelp.ithome.com.tw/upload/images/20210929/20134930AveNeNgZXu.jpg

★ d3.scalePoint 點比例尺

點比例尺跟區段比例尺很類似,但差別在於點比例尺是換算的是點的位置,區段比例尺則是換算區段的範圍
https://ithelp.ithome.com.tw/upload/images/20210929/20134930cgeXnjhZms.jpg

也因為兩個方法換算的方式不同,因此取出來的質也會有所差異

scaleBand

let bandScale = d3.scaleBand()
  .domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
  .range([0, 200]);

bandScale('狗'); // returns 0
bandScale('貓'); // returns 40
bandScale('海豚'); // returns 160

scalePoint

let pointScale = d3.scalePoint()
	  .domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
	  .range([0, 200]);

pointScale('狗');  // returns 0
pointScale('貓');  // returns 50
pointScale('海豚');  // returns 200

這樣有看出兩者的差異了嗎~瞭解 scalePoint 的用法後,我們也來看看它旗下的API,並且來進行一些相關細節設定吧

https://ithelp.ithome.com.tw/upload/images/20210929/20134930hviekLtffG.jpg

  • point.step( )

    這個方法使用來求取兩個point之間的距離

    let pointScale = d3.scalePoint()
    	  .domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
    	  .range([0, 200]);
    
    pointScale.step();  // returns 50 ,每個點之間的距離
    
  • point.padding( )

    這個方法是用來設定第一個點跟最後一個點分別對外的距離

    let pointScale = d3.scalePoint()
    	  .domain(['狗', '貓', '天竺鼠', '烏龜', '海豚'])
    	  .range([0, 200])
        .padding(3)
    

https://ithelp.ithome.com.tw/upload/images/20210929/20134930hZ4I0xvgqH.jpg


簡單比較

看完以上的分類有沒有很崩潰?我只是想畫圖表啊!為什麼要把比例尺搞得這麼複雜?別緊張別擔心,畫圖表時比較常用的比例尺其實也只有 Continuous ScaleOrdinal Scale 而已。我們最後再來簡單比較一下這兩種比例尺

https://ithelp.ithome.com.tw/upload/images/20210929/20134930NNErBbbnum.jpg

連續性比例尺:連續性的比例尺,適用於連續性質的資料,舉例來說:時間、數值;折線圖
非連續性比例尺:非連續性的比例尺,適用於非連續性質的資料,舉例來說:性別分為男、女;長條圖

以上!終於講完了我的天,scale 真的有許多小細節要注意,而且它的運作也相對複雜不少,但如果掌握好它的使用方法,畫起圖表來真的事半功倍!


Github Page 圖表與 Github 程式碼

最後附上本章的程式碼與圖表 GithubGithub Page,需要的人請自行取用~


上一篇
Day16-D3 的 Brush 刷子
下一篇
Day18-D3 的 Axis( ) & ticks( ) 軸線與刻度
系列文
三十天成為D3.js v7 好手30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言